class Environment {

  String shortname = "", name = "", commentary = "";
  Ecosystem eco;
  int Ncells = 1;
  Statevar time;
  Statevar[] vars = new Statevar[0];
  Flux[] fluxes = new Flux[0];
  FreeParam[] params = new FreeParam[0];
  Archive archive;

  Toolbar viewsToolbar = null, controller = null;
  SliderList paramSliders = null;
  HashMap views = new HashMap();
  View currentView = null;

// these don't belong here anymore
  float[] displayRect;
  boolean showArrows = true, showDetails = false;
  boolean showArrowsButton = false, showDetailsButton = false;
  
  boolean awakened = false;
  float internalTimestep = 0.01;
  float stopTime = Inf;
  boolean implicit = true; // method for derivatives in takeStep
  boolean clipAtMaxMin = false; // constrain variables to their max and min at each step, at the expense of tracer conservation
  boolean exitOnNan = false;
  boolean exitTriggered = false;
  
  
  Environment() {}
  
  Environment(Ecosystem eco) {
    linkToEcosystem(eco);
  }



  // initialization -------------------------------------------------------------------


  
  void linkToEcosystem(Ecosystem eco) {
    this.eco = eco;
    eco.allocate(Ncells);
    vars = (Statevar[])concat(vars, eco.vars);
    fluxes = (Flux[])concat(fluxes, eco.fluxes);
    params = (FreeParam[]) concat(params, eco.params);
    time = addVar("time","t","d",1);
    eco.time = time;
  }
  
  
  Statevar addVar(String name, String shortname, String units, int N) {
    Statevar V = new Statevar(name,shortname,units,N);
    vars = (Statevar[])append(vars,V);
    return V;
  }
  
  
  Flux addFlux(String name, String shortname, Statevar from, Statevar to, int N) {
    Flux F = new Flux(name,shortname,from,to,N);
    fluxes = (Flux[])append(fluxes,F);
    return F;
  }
  
  
  FreeParam addParam(String name, String shortname, String units, float initial, float min, float max) {
    FreeParam P = new FreeParam(name, shortname, units, initial, min, max);
    params = (FreeParam[])append(params,P);
    return P;
  }

  
  void wakeup() { // initializations that happen at the last possible moment before the model starts visibly running
    // tonicella 0.6 calls setUnits(time) for all fluxes here  
    awakened = true;
    resetParams(); // note: needs to happen before rewind()
    rewind();
    if (archive != null) archive.filename = shortname; // user probably set the shortname after constructing the Environment object.
                                                       // Note: now that baseOutputPath isn't a global variable, there's no automatic base path prepended to this    
  }

  
  void rewind() {
    for (int i=0; i<vars.length; i++) vars[i].rewind();
    calcFluxes();
    if (archive != null) archive.rewind(getCurrentState());
  }
 
  
  void updatePublicParams() {
    for (int i=0; i<params.length; i++) {
      if (params[i].slider != null) {
        params[i].current = params[i].slider.getVal();
      }
    }
  }
  

  void resetParams() {
    for (int i=0; i<params.length; i++) {
      params[i].current = params[i].initial;
      if (params[i].slider != null) {
        params[i].slider.setVal(params[i].current);
      }
    }
    updatePrivateParams();
  }
  

  
  // math -----------------------------------------------------------------------------
  

  
  void updatePrivateParams() {
    eco.updatePrivateParams();
    // *** anything that needs to be be updated after params are changed by the user ***
  }
 
 
  void calcFluxes() {
    eco.calcFluxes();
    // *** this is where advection, mixing, sinking, self-shading, etc go ***
    // *** this is also the time to calculate derived, non-dynamic variables ***
  }
  
  
  State getCurrentState() {
    State S = new State(this);
    for (int i=0; i<vars.length; i++) arrayCopy(vars[i].current, S.vars[i]);
    for (int i=0; i<fluxes.length; i++) arrayCopy(fluxes[i].current, S.fluxes[i]);
    for (int i=0; i<params.length; i++) S.params[i] = params[i].current;
    return S;
  }
  
  
  void setCurrentState(State S) {
    for (int i=0; i<vars.length; i++) arrayCopy(S.vars[i], vars[i].current);
    for (int i=0; i<fluxes.length; i++) arrayCopy(S.fluxes[i], fluxes[i].current);
    for (int i=0; i<params.length; i++) params[i].current = S.params[i];
  }
  
  
  void takeStep(float dt) {
    // the basic operation
    //    [vars] += [fluxes] * dt
    // works on the "current" fields of vars and fluxes
    time.current[0] += dt;
    if (exitTriggered) return; // once a nan is encountered, keep time moving forward but freeze the integration
    for (int j=0; j<fluxes.length; j++) {
      Flux F = fluxes[j];
      F.constrainCurrent();
      if (isnan(F.current)) {
        if (!exitTriggered) println(F.shortname + " = nan"); // only give the first of these errors
        if (exitOnNan) {
          archive.filename = "crash";
          archive.toFile();
          exit();
          exitTriggered = true;
        } else {
          exitTriggered = true; // clunky--the point is, even if not exiting, still want to suppress all but the first error
        }
      }
      for (int i=0; i<Ncells; i++) { 
        if (implicit && (F.current[i] > 0 && (F.from != null && F.from.current[i] != 0 && F.from.min==0))) { // implicit, from "from" to "to"
          // note: only does the implicit calculation if the statevar's .min field is set to make it positive definite 
          float adj = 1.0 / (1.0 + F.current[i] * dt / F.from.current[i]); // adjustment to conserve total tracer
          if (F.to != null) F.to.current[i] += F.current[i] * dt * adj;
          F.from.current[i] *= adj;
        } else if (implicit && (F.current[i] <= 0 && (F.to != null && F.to.current[i] != 0 && F.to.min==0))) { // implicit, reversing sign and from "to" to "from"
          // note: only does the implicit calculation if the statevar's .min field is set to make it positive definite 
          float adj = 1.0 / (1.0 - F.current[i] * dt / F.to.current[i]);
          if (F.from != null) F.from.current[i] -= F.current[i] * dt * adj;
          F.to.current[i] *= adj;
        } else { // simple, explicit version
          if (F.to != null) F.to.current[i] += F.current[i] * dt;
          if (F.from != null) F.from.current[i] -= F.current[i] * dt;
        }
        if (clipAtMaxMin) {
          if (F.to != null) F.to.current[i] = constrain(F.to.current[i], F.to.min, F.to.max);
          if (F.from != null) F.from.current[i] = constrain(F.from.current[i], F.from.min, F.from.max);
        }
      }
    }
    calcFluxes();
  }


  State takeStep(State S, float dt) {
    // this version takes a step from one arbitrary state to another, using "current" as scratch space
    // (would use this, along with getCurrentState() and setCurrentState(), to Runge-Kutta, for example)
    setCurrentState(S);
    takeStep(dt);
    return getCurrentState();
  }
    
    
  String integrate(float dt) {
    String errorMsg = "";
    // this is where to put a fancy timestepping routine
    int Nsteps = floor(dt / internalTimestep);
    float leftover = dt - internalTimestep * Nsteps;
    for (int n=0; n<Nsteps; n++) {
      takeStep(internalTimestep);
    }
    if (abs(leftover) > 1e-6 * dt) {
      takeStep(leftover);
    }
    if (archive != null) archive.offer(getCurrentState());
    return errorMsg;
  }
  
  
  
  // interaction with gui and views ------------------------------------------------------------------ 
  
  
  void toggleArrows() {showArrows = !showArrows;}
  
  void toggleDetails() {showDetails = !showDetails;}  
  
  void addView(View view) {
    views.put(view.shortname, view);
    if (currentView==null) currentView = view;
  }

 
  // archiving -------------------------------------------------------------------------
 
  
  
  void startArchive(float dt) {
    if (online) return;
    archive = new Archive(shortname, dt, this);
  }
  
  void saveArchive() {
    if (archive==null) return;
    archive.toFile();
  }

  
  
  
  // utility ---------------------------------------------------------------------------
  
  
  
  Statevar getVar(String S) {
    for (int i=0; i<vars.length; i++) if (vars[i].shortname.equals(S)) return vars[i];
    return null;
  }
  
  Flux getFlux(String S) {
    for (int i=0; i<fluxes.length; i++) if (fluxes[i].shortname.equals(S)) return fluxes[i];
    return null;
  }  
  
  FreeParam getParam(String S) {
    for (int i=0; i<params.length; i++) if (params[i].shortname.equals(S)) return params[i];
    return null;
  }
  
  int getVarIndex(String S) {
    for (int i=0; i<vars.length; i++) if (vars[i].shortname.equals(S)) return i;
    return -1;
  }
  
  int getFluxIndex(String S) {
    for (int i=0; i<fluxes.length; i++) if (fluxes[i].shortname.equals(S)) return i;
    return -1;
  }  
  
  int getParamIndex(String S) {
    for (int i=0; i<params.length; i++) if (params[i].shortname.equals(S)) return i;
    return -1;
  }

  float getParamVal(String S) {
    for (int i=0; i<params.length; i++) if (params[i].shortname.equals(S)) return params[i].current;
    println("warning: param " + S + " not found; returning NaN");
    return 0./0;
  }
  
  Object get(String S) {
    Statevar V = getVar(S);
    if (V != null) return V;
    FreeParam P = getParam(S);
    if (P != null) return P;
    return getFlux(S);
  }
  
  


}

